/*******************************************************************************
* Copyright (c) 2015 Development Gateway, Inc and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the MIT License (MIT)
* which accompanies this distribution, and is available at
* https://opensource.org/licenses/MIT
*
* Contributors:
* Development Gateway - initial API and implementation
*******************************************************************************/
package org.devgateway.toolkit.forms.wicket.page.reports;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.authroles.authentication.AuthenticatedWebApplication;
import org.apache.wicket.extensions.ajax.markup.html.AjaxLazyLoadPanel;
import org.apache.wicket.markup.IMarkupCacheKeyProvider;
import org.apache.wicket.markup.IMarkupResourceStreamProvider;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.SharedResourceReference;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
import org.apache.wicket.util.resource.IResourceStream;
import org.apache.wicket.util.resource.StringResourceStream;
import org.devgateway.toolkit.forms.util.FolderContentResource;
import org.devgateway.toolkit.forms.util.MarkupCacheService;
import org.devgateway.toolkit.forms.wicket.page.BasePage;
import org.devgateway.toolkit.forms.wicket.styles.BlockUiReportsJavaScript;
import org.devgateway.toolkit.forms.wicket.styles.ReportsStyles;
import org.devgateway.toolkit.reporting.ReportUtil;
import org.pentaho.reporting.engine.classic.core.MasterReport;
import org.pentaho.reporting.engine.classic.core.ReportProcessingException;
import org.pentaho.reporting.engine.classic.core.layout.output.AbstractReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.base.PageableReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.pdf.PdfOutputProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.base.FlowReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.base.StreamReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.html.AllItemsHtmlPrinter;
import org.pentaho.reporting.engine.classic.core.modules.output.table.html.HtmlOutputProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.html.HtmlPrinter;
import org.pentaho.reporting.engine.classic.core.modules.output.table.html.StreamHtmlOutputProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.rtf.FlowRTFOutputProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.xls.FlowExcelOutputProcessor;
import org.pentaho.reporting.libraries.repository.ContentIOException;
import org.pentaho.reporting.libraries.repository.ContentLocation;
import org.pentaho.reporting.libraries.repository.DefaultNameGenerator;
import org.pentaho.reporting.libraries.repository.file.FileRepository;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
/**
* @author mihai This is the base class for Pentaho reports displayed in Wicket.
* If you need filters, please use {@link AbstractFilteredReportPage}
*/
public abstract class AbstractReportPage extends BasePage {
private static final long serialVersionUID = -7471616582122767104L;
/**
* start-key used to identify the reports markup
*/
private static final String START_NAME_REPORT_KEY = "REPORTMARKUP";
/**
* The name of the .prpt report file
*/
protected String reportResourceName;
/**
* Should we cache the HTML, PDF, Word, Excel for this report?
*/
protected Boolean caching = true;
@SpringBean
protected MarkupCacheService markupCacheService;
/**
* A special Wicket panel that displays the stream coming from Pentaho's
* {@link AbstractReportProcessor}
*/
protected ResourceStreamPanel htmlReportPanel;
protected ReportDownloadLink pdfDownload;
protected ReportDownloadLink xlsDownload;
protected ReportDownloadLink rtfDownload;
/**
* @return true if this report page can be displayed without further
* conditions. Possible conditions include checking the report
* parameters were provided.
*/
public boolean canRenderReport() {
return true;
}
/**
* A generic download link component
*
* @author mpostelnicu See
* http://blog.jdriven.com/2013/05/wicket-quick-tips-create-a-download-link/
*/
public class ReportDownloadLink extends Link<Void> {
private static final long serialVersionUID = 2227561249211024004L;
private OutputType outputType;
private Map<OutputType, String> outputExtension = new HashMap<>();
public ReportDownloadLink(final String id, final OutputType outputType) {
super(id);
this.outputType = outputType;
outputExtension.put(OutputType.PDF, ".pdf");
outputExtension.put(OutputType.EXCEL, ".xls");
outputExtension.put(OutputType.RTF, ".doc");
setOutputMarkupId(true);
setOutputMarkupPlaceholderTag(true);
}
@Override
protected void onConfigure() {
setVisible(canRenderReport());
super.onConfigure();
}
@Override
public void onClick() {
AbstractResourceStreamWriter rstream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1147133679640377670L;
@Override
public void write(final OutputStream output) throws IOException {
try {
if (canRenderReport()) {
// first try to fetch the report from cache,
// otherwise create the report and cache it
byte[] reportContent = markupCacheService.getReportFromCache(outputType.name(),
FilenameUtils.getName(AbstractReportPage.this.reportResourceName).replace(".prpt",
""),
getPageParameters().toString());
if (reportContent == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
generateReport(outputType, baos);
reportContent = baos.toByteArray();
if (caching) {
markupCacheService
.addReportToCache(outputType.name(),
FilenameUtils.getName(AbstractReportPage.this.reportResourceName)
.replace(".prpt", ""),
getPageParameters().toString(), reportContent);
}
}
output.write(reportContent);
}
} catch (IllegalArgumentException | ReportProcessingException e) {
e.printStackTrace();
}
}
};
ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(rstream,
FilenameUtils.getName(AbstractReportPage.this.reportResourceName).replace(".prpt",
outputExtension.get(outputType)));
getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
}
}
/**
* A {@link Panel} that instead of its markup stream returns another
* {@link OutputStream}
*
* See http
* ://wicketbypranav.blogspot.ro/2013/05/throw-outputstream-html-data
* -in-wicket.html
*
* @author mpostelnicu
*
*/
public class ResourceStreamPanel extends Panel implements IMarkupResourceStreamProvider, IMarkupCacheKeyProvider {
private static final long serialVersionUID = 6760926366225472057L;
private AbstractReportPage parent;
public ResourceStreamPanel(final String id, final AbstractReportPage parent) {
super(id);
this.parent = parent;
setOutputMarkupId(true);
}
/**
* Implementing getMarkupResourceStream() method to return byte[] data
* coming from reports generator.
*/
@Override
public IResourceStream getMarkupResourceStream(final MarkupContainer container, final Class<?> containerClass) {
StringBuilder panelMarkup = new StringBuilder();
panelMarkup.append("<wicket:panel wicket:id='panel'>");
ByteArrayOutputStream htmlStreamData = new ByteArrayOutputStream();
try {
if (canRenderReport()) {
generateReport(OutputType.HTML, htmlStreamData);
}
} catch (IllegalArgumentException | ReportProcessingException e) {
e.printStackTrace();
}
String content = new String(htmlStreamData.toByteArray());
panelMarkup.append(content);
panelMarkup.append("</wicket:panel>");
return new StringResourceStream(panelMarkup.toString());
}
@Override
public void onAfterRender() {
super.onAfterRender();
// unblock the UI after we render the HTML report
AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class);
if (target != null) {
target.appendJavaScript("$.unblockUI();");
}
}
/**
* Implementing getCacheKey() method of 'IMarkupCacheKeyProvider'
* interface.
*/
public String getCacheKey(final MarkupContainer container, final Class<?> containerClass) {
// return a key based on page parameters and report class name,
// also a common start name so that we can identify them
// (if null is returned then the markup is not cached)
if (caching) {
return START_NAME_REPORT_KEY + "-" + getPageParameters().toString() + "-"
+ this.parent.getClass().getSimpleName();
}
return null;
}
}
/**
* The supported output types
*/
public enum OutputType {
PDF, EXCEL, HTML, RTF
}
public AbstractReportPage(final String reportResourceName, final PageParameters pageParameters) {
super(pageParameters);
this.reportResourceName = reportResourceName;
}
@Override
public void onInitialize() {
super.onInitialize();
add(new AjaxLazyLoadPanel("htmlReportPanel") {
private static final long serialVersionUID = 5167275188250433603L;
@Override
public Component getLazyLoadComponent(final String id) {
htmlReportPanel = new ResourceStreamPanel(id, AbstractReportPage.this);
return htmlReportPanel;
}
});
pdfDownload = new ReportDownloadLink("pdfDownload", OutputType.PDF);
add(pdfDownload);
xlsDownload = new ReportDownloadLink("xlsDownload", OutputType.EXCEL);
add(xlsDownload);
rtfDownload = new ReportDownloadLink("rtfDownload", OutputType.RTF);
add(rtfDownload);
}
/**
* Returns the report definition used by this report generator. If this
* method returns <code>null</code>, the report generation process will
* throw a <code>NullPointerException</code>.
*
* @return the report definition used by thus report generator
*/
public MasterReport getReportDefinition() {
try {
// Using the classloader, get the URL to the reportDefinition file
final ClassLoader classloader = this.getClass().getClassLoader();
final URL reportDefinitionURL = classloader.getResource(reportResourceName);
// Parse the report file
final ResourceManager resourceManager = new ResourceManager();
final Resource directly = resourceManager.createDirectly(reportDefinitionURL, MasterReport.class);
return (MasterReport) directly.getResource();
} catch (ResourceException e) {
e.printStackTrace();
}
return null;
}
/**
* Returns the set of parameters that will be passed to the report
* generation process. If there are no parameters required for report
* generation, this method may return either an empty or a <code>null</code>
* <code>Map</code>
*
* @return the set of report parameters to be used by the report generation
* process, or <code>null</code> if no parameters are required.
*/
public abstract Map<String, Object> getReportParameters();
/**
* Generates the report in the specified <code>outputType</code> and writes
* it into the specified <code>outputStream</code>.
*
* It is the responsibility of the caller to close the
* <code>outputStream</code> after this method is executed.
*
* @param outputType
* the output type of the report (HTML, PDF, HTML)
* @param outputStream
* the stream into which the report will be written
* @throws IllegalArgumentException
* indicates the required parameters were not provided
* @throws ReportProcessingException
* indicates an error generating the report
*/
public void generateReport(final OutputType outputType, final OutputStream outputStream)
throws IllegalArgumentException, ReportProcessingException {
if (outputStream == null) {
throw new IllegalArgumentException("The output stream was not specified");
}
// Get the report and data factory
final MasterReport report = getReportDefinition();
// Add any parameters to the report
final Map<String, Object> reportParameters = getReportParameters();
if (reportParameters == null) {
return;
}
for (String key : reportParameters.keySet()) {
report.getParameterValues().put(key, reportParameters.get(key));
}
// Prepare to generate the report
AbstractReportProcessor reportProcessor = null;
try {
// Greate the report processor for the specified output type
switch (outputType) {
case PDF:
final PdfOutputProcessor targetPdf = new PdfOutputProcessor(report.getConfiguration(),
outputStream, report.getResourceManager());
reportProcessor = new PageableReportProcessor(report, targetPdf);
reportProcessor.processReport();
break;
case EXCEL:
final FlowExcelOutputProcessor targetExcel = new FlowExcelOutputProcessor(report.getConfiguration(),
outputStream, report.getResourceManager());
reportProcessor = new FlowReportProcessor(report, targetExcel);
reportProcessor.processReport();
break;
case RTF:
final FlowRTFOutputProcessor targetRtf = new FlowRTFOutputProcessor(report.getConfiguration(),
outputStream, report.getResourceManager());
reportProcessor = new FlowReportProcessor(report, targetRtf);
reportProcessor.processReport();
break;
case HTML:
ContentLocation targetRoot = null;
File tempDir = null;
try {
// we manually make the folder to drop all exported html
// files into
tempDir = ReportUtil.createTemporaryDirectory("tmpreport");
targetRoot = new FileRepository(tempDir).getRoot();
} catch (ContentIOException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// we create a folder content resource for the entire tmpdir.
// This dir will only hold the fields for this export
FolderContentResource fcr = new FolderContentResource(tempDir);
// we always have an authenticated web app
AuthenticatedWebApplication authApp = (AuthenticatedWebApplication) getApplication();
// we add the folder resource as a shared resource
authApp.getSharedResources().add(tempDir.getName(), fcr);
SharedResourceReference folderResourceReference = new SharedResourceReference(tempDir.getName());
authApp.mountResource(tempDir.getName(), folderResourceReference);
final HtmlOutputProcessor outputProcessor =
new StreamHtmlOutputProcessor(report.getConfiguration());
final HtmlPrinter printer = new AllItemsHtmlPrinter(report.getResourceManager());
printer.setContentWriter(targetRoot, new DefaultNameGenerator(targetRoot, "index", "html"));
printer.setDataWriter(targetRoot, new DefaultNameGenerator(targetRoot, "content")); //$NON-NLS-1$
// we use a special URL Rewriter that knows how to speak Wicket
// :-)
printer.setUrlRewriter(new WicketResourceURLRewriter(folderResourceReference));
outputProcessor.setPrinter(printer);
reportProcessor = new StreamReportProcessor(report, outputProcessor);
reportProcessor.processReport();
// we plug the html file stream into the output stream
FileInputStream indexFileStream =
new FileInputStream(tempDir.getAbsolutePath() + File.separator + "index.html");
IOUtils.copy(indexFileStream, outputStream);
indexFileStream.close();
break;
default:
throw new RuntimeException("Unknown output type provided!");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reportProcessor != null) {
reportProcessor.close();
}
}
}
@Override
public void renderHead(final IHeaderResponse response) {
super.renderHead(response);
// block UI for reports page
response.render(JavaScriptHeaderItem.forReference(BlockUiReportsJavaScript.INSTANCE));
// Load Reports Styles.
response.render(CssHeaderItem.forReference(ReportsStyles.INSTANCE));
}
}